// Permission to use, copy, modify, and/or distribute this software for
// any purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
// FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
// DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

// Ported from the Tcl/Tk version.  See the original for more documentation.

var bokpass = (function () {
  "use strict";

  // JSON version of bok::strings
  var strings = {
    "en": {
      "games": {
        "jp1": "Bokura no Taiyō",
        "jp2": "Zoku Bokura no Taiyō: Taiyō Shōnen Jango",
        "jp3": "Shin Bokura no Taiyō: Gyakushū no Sabata",
        "jp4": "Bokura no Taiyō: Django & Sabata",
        "en1": "Boktai: The Sun Is in Your Hand",
        "en2": "Boktai 2: Solar Boy Django",
        "en3": "Boktai 3: Sabata's Counterattack",
        "en4": "Lunar Knights"
      },
      "labels": {
        "game-tabs": [
          { game: 1, name: "Boktai" },
          { game: 2, name: "Boktai 2 / Zoktai" },
          { game: 3, name: "Boktai 3 / Shinbok" },
          { game: 4, name: "Lunar Knights / Boktai DS" }
        ],
        "keyboard-tabs": [
          { key: "en64", name: "Base64" },
          { key: "lk64", name: "Base64 LK" },
          { key: "jp64", name: "Base64 JP" },
          { key: "en", name: "English" },
          { key: "ext", name: "Latin Ext." },
          { key: "hiri", name: "Hirigana" },
          { key: "kata", name: "Katakana" }
        ],
        "padding": "Padding",             "header-padding": "Padding",
        "region": "Region",               "region-name": "Region",
        "checksum": "Checksum",           "offset": "Offset",
        "sol": "Sol",                     "sol/4": "Sol",
        "sol/4096": "Sol",
        "loan": "Loan",                   "loan/256": "Loan",
        "timezone": "Timezone",           "timezone-name": "Timezone",
        "hours": "Hours",                 "minutes": "Minutes",
        "difficulty": "Difficulty",       "difficulty-name": "Difficulty",
        "dungeons": "Dungeons",           "clears": "Clears",
        "continues": "Continues",         "caught": "Caught",
        "kills": "Kills",                 "kills/8": "Kills",
        "rank": "Rank",                   "rank-name": "Rank",
        "forges": "Forges",               "races": "Races",
        "link-battles": "Link-Battles",   "link-trades": "Link-Trades",
        "link-shopping": "Link-Shopping", "link-races": "Link-Races",
        "cross-linked": "Cross-Linked",
        "title": "Title",                 "title-name": "Title",
        "titles": "Titles",               "title-list": "Titles",
        "side": "Side",                   "side-name": "Side",
        "style": "Style",                 "style-name": "Style",
        "sword": "Sword",                 "sword-name": "Sword",
        "gun": "Gun",                     "gun-name": "Gun",
        "terrennial": "Terrennial",       "terrennial-name": "Terrennial",
        "climate": "Climate",             "climate-name": "Climate",
        "endings": "Survivors",           "ending-list": "Survivors",
        "name-dark": "Name(dark)",        "name-solar": "Name(sol)",
        "name": "Name"
      },
      "regions": [
        "Unknown Region #0", "Japan", "North America", "Europe"
      ],
      "timezones": {
        "jp": [
          "Unknown Timezone #0",
          "Ibaraki",   "Tochigi",      "Gunma",    "Saitama",
          "Chiba",     "Tokyo",        "Kanagawa", "Ogasawara",
          "Niigata",   "Toyama",       "Ishikawa", "Fukui",
          "Yamanashi", "Nagano",       "Gifu",     "Shizuoka",
          "Aichi",     "Mie",          "Shiga",    "Kyoto",
          "Osaka",     "Hyougo",       "Nara",     "Wakayama",
          "Tottori",   "Shimane",      "Okayama",  "Hiroshima",
          "Yamaguchi", "Tokushima",    "Kagawa",   "Ehime",
          "Kouchi",    "Fukuoka",      "Saga",     "Nagasaki",
          "Kumamoto",  "Ooita",        "Miyazaki", "Kagoshima",
          "Okinawa",   "Ishigakijima", "Sapporo",  "Hakodate",
          "Asahikawa", "Kushiro",      "Obihiro",  "Aomori",
          "Iwate",     "Miyagi",       "Akita",    "Yamagata",
          "Fukushima"
        ],
        "na": [
          "Unknown Timezone #0",
          "St. John's",   "Labrador City",  "Halifax",         "Quebec",
          "Montreal",     "Ottawa",         "Toronto",         "Timmins",
          "Boston",       "Albany",         "Syracuse",        "New York",
          "Philadelphia", "Pittsburgh",     "Washington D.C.", "Norfolk",
          "Raleigh",      "Charlotte",      "Atlanta",         "Jacksonville",
          "Tampa",        "Miami",          "Detroit",         "Cleveland",
          "Columbus",     "Lexington",      "Thunder Bay",     "Winnipeg",
          "Regina",       "Thompson",       "Indianapolis",    "Chicago",
          "Milwaukee",    "Minneapolis",    "Bismark",         "St. Louis",
          "Nashville",    "Memphis",        "Montgomery",      "Jackson",
          "New Orleans",  "Des Moines",     "Lincoln",         "Kansas City",
          "Topeka",       "Springfield",    "Little Rock",     "Oklahoma City",
          "Dallas",       "Houston",        "Edmonton",        "Calgary",
          "Yellowknife",  "Denver",         "Albuquerque",     "Phoenix",
          "Boise",        "Salt Lake City", "Vancouver",       "Whitehorse",
          "Spokane",      "Seattle",        "Salem",           "Reno",
          "Las Vegas",    "Los Angeles",    "San Diego",       "San Francisco",
          "Anchorage",    "Fairbanks",      "Ketchikan",       "Honolulu"
        ],
        "eu": [
          "Unknown Timezone #0",
          "Reykjavik",  "Dublin",       "Cork",      "London",
          "Cardiff",    "Edinburgh",    "Belfast",   "Liverpool",
          "Lisbon",     "Porto",        "Valletta",  "Madrid",
          "Barcelona",  "Valencia",     "La Coruna", "Seville",
          "Paris",      "Brest",        "Lyons",     "Bordeaux",
          "Marseilles", "Brussels",     "Bastogne",  "Amsterdam",
          "Rotterdam",  "Luxembourg",   "Berlin",    "Hamburg",
          "Essen",      "Frankfurt",    "Munich",    "Bern",
          "Geneve",     "Vaduz",        "Wien",      "Innsbruck",
          "Rome",       "Genova",       "Venezia",   "Palermo",
          "Sassari",    "Oslo",         "Bergen",    "Trondheim",
          "Copenhagen", "Odense",       "Stockholm", "Gothenburg",
          "Helsinki",   "Turku",        "Mikkeli",   "Warszawa",
          "Gdansk",     "Poznan",       "Wroclaw",   "Krakow",
          "Praha",      "Bratislava",   "Kosice",    "Budapest",
          "Bucuresti",  "Ljubljana",    "Zagreb",    "Sarajevo",
          "Beograd",    "Skopje",       "Tirane",    "Sofiya",
          "Athinai",    "Thessaloniki", "Iraklion",  "Ankara",
          "Istanbul",   "Izmir",        "Konya",     "Adana",
          "Jerusalem",  "Pretoria",     "Cape Town", "Durban",
          "Wellington", "Auckland",     "Dunedin",   "Sydney",
          "Melbourne",  "Adelaide",     "Perth",     "Brisbane"
        ]
      },
      "ranks": [
        "S", "A+", "A",  "A-", "B+", "B",  "B-", "C+",
        "C", "C-", "D+", "D",  "D-", "F+", "F",  "F-"
      ],
      "difficulties": {
        "1": ["Easy", "Normal 1", "Normal 2", "Hard"],
        "2": [],
        "3": [],
        "4": ["Normal", "Hard", "Nightmare"]
      },
      "titles": {
        "1": [
          "Trigger of Sol", "Gun Master",     "Gladiator",   "Bishop",
          "Queen",          "Berserker",      "Death",       "Solar Boy",
          "Dark Boy",       "Solar Menchant", "Running Boy", "King",
          "Rook",           "Knight",         "Pawn"
        ],
        "2": [
          "Sword Master", "Spear Master", "Hammer Master", "Fist Master",
          "Gun Master",   "Adept",        "Day Walker",    "Adventurer",
          "Agent",        "Collector",    "Dark Hunter",   "Grand Master"
        ],
        "3": [
          "Adept",       "Gladiator", "SP Agent",  "Champion",
          "Dark Hunter", "Alchemist", "Collector", "Doll Master",
          "Storyteller", "Grandmaster"
        ],
        "4": [
          "Dark Knight",   "Sol Gunner",      "Sword Master",  "Gun Master",
          "Guardian",      "Treasure Hunter", "Collector",     "Huntmaster",
          "Shooting Star", "Gladiator",       "Special Agent", "Wanderer",
          "Adventurer",    "Grand Master"
        ]
      },
      "sides": ["Red", "Black", "Grey"],
      "styles": ["Sword", "Spear", "Hammer", "Gun", "Fists", "No Style"],
      "endings": ["Otenko", "Everybody", "Nobody", "Sabata"],
      "swords": ["Vanargand", "Jormungandr", "Hel"],
      "guns": ["Knight", "Dragoon", "Bomber", "Witch", "Ninja"],
      "terrennials": [
        "Toasty", "Nero", "Ursula", "Ezra", "Alexander", "Tove", "War Rock"
      ],
      "climates": [
        "Balmy Sub-Tropical", "Arid Desert", "Tropical Rainforest",
        "Humind-Continental", "Frigid Arctic"
      ]
    }
  };

  // JSON version of bok::ctable
  var ctable = {
    "0x0": [
      "�", "�", "�",  "�", "�", "�", "�", "�",
      "�", "�", "�",  "�", "�", "�", "�", "�",
      "�", "�", "�",  "�", "�", "�", "�", "�",
      "�", "�", "�",  "�", "�", "�", "�", "�",
      " ", "!", "\"", "#", "÷", "%", "&", "'",
      "(", ")", "*",  "+", ",", "-", ".", "/",
      "0", "1", "2",  "3", "4", "5", "6", "7",
      "8", "9", ":",  ";", "<", "=", ">", "?",
      "@", "A", "B",  "C", "D", "E", "F", "G",
      "H", "I", "J",  "K", "L", "M", "N", "O",
      "P", "Q", "R",  "S", "T", "U", "V", "W",
      "X", "Y", "Z",  "[", "×", "]", "^", "_",
      "`", "a", "b",  "c", "d", "e", "f", "g",
      "h", "i", "j",  "k", "l", "m", "n", "o",
      "p", "q", "r",  "s", "t", "u", "v", "w",
      "x", "y", "z",  "{", "|", "}", "¯", "⋅"
    ],
    "0x1F": [
      "�", "Ä", "�", "Ç", "É", "Ñ", "Ö", "Ü",
      "á", "à", "â", "ä", "�", "å", "ç", "é",
      "è", "ê", "ë", "í", "ì", "î", "ï", "ñ",
      "ó", "ò", "ô", "ö", "�", "ú", "ù", "û",
      "ü", "�", "°", "�", "�", "�", "�", "�",
      "ß", "�", "�", "�", "�", "�", "�", "�",
      "�", "�", "�", "�", "�", "�", "�", "�",
      "�", "�", "�", "�", "�", "�", "�", "�",
      "�", "¿", "¡", "�", "�", "�", "�", "�",
      "«", "»", "�", "�", "À", "�", "�", "Œ",
      "œ", "�", "�", "�", "�", "�", "�", "�",
      "ý", "ÿ", "Ý", "�", "�", "�", "�", "�",
      "�", "�", "�", "�", "�", "�", "Â", "Ê",
      "Á", "Ë", "È", "Í", "Î", "Ï", "Ì", "Ó",
      "Ô", "�", "Ò", "Ú", "Û", "Ù", "�", "�",
      "�", "�", "�", "�", "�", "�", "�", "�"
    ],
    "0x80": [
      "�",  "あ", "い", "う", "え", "お", "か", "き",
      "く", "け", "こ", "さ", "し", "す", "せ", "そ",
      "よ", "た", "ち", "つ", "て", "と", "な", "に",
      "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
      "下", "ま", "み", "む", "め", "も", "や", "ゆ",
      "よ", "ら", "り", "る", "れ", "ろ", "わ", "を",
      "左", "ん", "ぁ", "ぃ", "ぅ", "ぇ", "ぉ", "っ",
      "ゃ", "ゅ", "ょ", "が", "ぎ", "ぐ", "げ", "ご",
      "右", "ざ", "じ", "ず", "ぜ", "ぞ", "だ", "ぢ",
      "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ",
      "東", "ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "｡",  "､",
      "~",  "ー", "…",  "�",  "�",  "�",  "�",  "�",
      "西", "ア", "イ", "ウ", "エ", "オ", "カ", "キ",
      "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ",
      "南", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ",
      "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
      "北", "マ", "ミ", "ム", "メ", "モ", "ラ", "リ",
      "ル", "レ", "ロ", "ヤ", "ユ", "ヨ", "ワ", "ヲ",
      "大", "ン", "ァ", "ィ", "ゥ", "ェ", "ォ", "ッ",
      "ャ", "ュ", "ョ", "ガ", "ギ", "グ", "ゲ", "ゴ",
      "中", "ザ", "ジ", "ズ", "ゼ", "ゾ", "ダ", "ヂ",
      "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
      "小", "パ", "ピ", "プ", "ペ", "ポ", "・", "：",
      "；", "「", "」", "＋", "×",  "℃",  "℉",  "�",
      "�",  "↑",  "↓",  "→",  "←",  "★",  "♥",  "♪",
      "ヴ", "Ⅰ",  "Ⅱ",  "Ⅲ",  "�",  "�",  "�",  "�",
      "風", "白", "黒", "赤", "青", "黄", "緑", "金",
      "銀", "紫", "�", "火", "炎", "災", "水", "氷",
      "永", "太", "陽", "年", "月", "日", "時", "分",
      "秒", "春", "夏", "秋", "冬", "之", "ヶ", "々",
      "＝", "丈", "片", "己", "凶", "歯", "�",  "�",
      "�",  "�",  "�",  "�",  "�",  "�",  "�",  "�"
    ],
    "0x180": [
      "�",  "あ", "い", "う", "え", "お", "か", "き",
      "く", "け", "こ", "さ", "し", "す", "せ", "そ",
      "よ", "た", "ち", "つ", "て", "と", "な", "に",
      "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
      "下", "ま", "み", "む", "め", "も", "や", "ゆ",
      "よ", "ら", "り", "る", "れ", "ろ", "わ", "を",
      "左", "ん", "ぁ", "ぃ", "ぅ", "ぇ", "ぉ", "っ",
      "ゃ", "ゅ", "ょ", "が", "ぎ", "ぐ", "げ", "ご",
      "右", "ざ", "じ", "ず", "ぜ", "ぞ", "だ", "ぢ",
      "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ",
      "東", "ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "｡",  "､",
      "~",  "ー", "…",  "�",  "�",  "�",  "�",  "�",
      "西", "ア", "イ", "ウ", "エ", "オ", "カ", "キ",
      "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ",
      "南", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ",
      "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
      "北", "マ", "ミ", "ム", "メ", "モ", "ラ", "リ",
      "ル", "レ", "ロ", "ヤ", "ユ", "ヨ", "ワ", "ヲ",
      "大", "ン", "ァ", "ィ", "ゥ", "ェ", "ォ", "ッ",
      "ャ", "ュ", "ョ", "ガ", "ギ", "グ", "ゲ", "ゴ",
      "中", "ザ", "ジ", "ズ", "ゼ", "ゾ", "ダ", "ヂ",
      "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
      "小", "パ", "ピ", "プ", "ペ", "ポ", "・", "：",
      "；", "「", "」", "＋", "×",  "℃",  "℉",  "�",
      "�",  "↑",  "↓",  "→",  "←",  "★",  "♥",  "♪",
      "ヴ", "Ⅰ",  "Ⅱ",  "Ⅲ",  "�",  "�",  "�",  "�",
      "風", "白", "黒", "赤", "青", "黄", "緑", "金",
      "銀", "紫", "�",  "火", "炎", "災", "水", "氷",
      "永", "太", "陽", "年", "月", "日", "時", "分",
      "秒", "春", "夏", "秋", " ",  "之", "ヶ", "々",
      "＝", "丈", "片", "己", "凶", "歯", "�",  "�",
      "�",  "�",  "�",  "�",  "�",  "�",  "�",  "�"
    ],
    "0x81": [
      "�",  "地", "均", "坂", "塔", "境", "塊", "填",
      "場", "増", "堀", "堤", "壊", "塚", "域", "城",
      "�",  "現", "理", "球", "環", "�",  "�",  "�",
      "�",  "切", "�",  "功", "攻", "項", "崎", "靖",
      "端", "化", "代", "付", "何", "仕", "任", "仗",
      "仲", "伯", "件", "作", "伝", "休", "体", "仮",
      "住", "佐", "他", "使", "便", "信", "倍", "借",
      "価", "値", "低", "侮", "個", "保", "係", "供",
      "侵", "依", "偉", "備", "偽", "似", "俊", "傷",
      "像", "優", "候", "修", "例", "側", "倒", "働",
      "健", "併", "佳", "倫", "停", "傲", "儀", "�",
      "�",  "�",  "�",  "�",  "�",  "�",  "�",  "�",
      "衝", "行", "往", "彼", "役", "徐", "復", "後",
      "待", "得", "徳", "術", "街", "御", "徴", "徹",
      "衛", "打", "払", "押", "択", "技", "抜", "投",
      "抗", "持", "担", "指", "捨", "排", "抵", "挑",
      "推", "提", "携", "授", "接", "掘", "操", "揮",
      "捕", "探", "換", "振", "掛", "援", "拠", "損",
      "拡", "把", "握", "掃", "撤", "�",  "�",  "�",
      "�",  "�",  "�",  "�",  "�",  "�",  "�",  "�",
      "状", "牧", "物", "特", "犠", "牲", "独", "狙",
      "猛", "狂", "狩", "猫", "狐", "狼", "獲", "猟",
      "獄", "性", "快", "悦", "怪", "悟", "怖", "情",
      "慎", "慢", "燐", "憶", "�",  "�",  "�",  "�",
      "�",  "粒", "料", "粗", "精", "�",  "�",  "�",
      "�",  "灯", "灼", "焼", "煙", "燥", "燃", "爆",
      "燼", "札", "材", "林", "杯", "村", "析", "相",
      "枚", "板", "松", "根", "格", "槍", "横", "株",
      "様", "棺", "桶", "桿", "植", "橋", "構", "機",
      "械", "�",  "欄", "樹", "樋", "椛", "椿", "模",
      "根", "標", "�",  "�",  "�",  "和", "利", "科",
      "称", "程", "種", "移", "秘", "積", "稼", "稲"
    ],
    "0x82": [
      "�",  "礼", "祈", "社", "祝", "神", "視", "福",
      "�",  "�",  "�",  "�",  "初", "裕", "複", "被",
      "捕", "紅", "紀", "約", "紋", "紙", "細", "組",
      "統", "終", "純", "練", "級", "緒", "経", "絵",
      "給", "絃", "納", "紹", "絡", "結", "続", "継",
      "絶", "編", "縁", "博", "織", "総", "縦", "綾",
      "締", "績", "網", "縮", "絆", "幻", "郷", "�",
      "�",  "次", "冷", "凍", "�",  "�",  "議", "論",
      "訳", "計", "討", "記", "許", "訓", "詳", "説",
      "話", "証", "読", "設", "語", "談", "試", "調",
      "誤", "課", "誠", "誘", "護", "認", "謙", "誕",
      "識", "謝", "泡", "汚", "浪", "液", "涙", "汰",
      "沙", "江", "況", "沢", "泊", "河", "注", "洋",
      "泣", "治", "活", "浴", "浩", "池", "波", "洗",
      "流", "法", "決", "油", "消", "温", "浮", "海",
      "�",  "�",  "�",  "�",  "�",  "�",  "�",  "�"
    ]
  };

  // JSON version of bok::b64table
  var b64table = {
    "jp": [
      "あ", "い", "う", "え", "お", "か", "き", "く",
      "け", "こ", "さ", "し", "す", "せ", "そ", "た",
      "ち", "つ", "て", "と", "な", "に", "ぬ", "ね",
      "の", "は", "ひ", "ふ", "へ", "ほ", "ま", "み",
      "む", "め", "も", "や", "ゆ", "よ", "ら", "り",
      "る", "れ", "ろ", "わ", "を", "が", "ぎ", "ぐ",
      "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ", "だ",
      "ぢ", "づ", "で", "ど", "ば", "び", "ぶ", "べ",  "ぼ"
    ],
    "en": [
      "B", "C", "D", "F", "G", "H", "J", "K",
      "L", "M", "N", "P", "Q", "R", "S", "T",
      "V", "W", "X", "Y", "Z",
      "b", "c", "d", "f", "g", "h", "j", "k",
      "l", "m", "n", "p", "q", "r", "s", "t",
      "v", "w", "x", "y", "z",
      "0", "1", "2", "3", "4", "5", "6", "7",
      "8", "9", "?", "!", "@", "#", "=", "^",
      ">", "/", "-", "_", "+", ":", "."
    ],
    "lk": [
      "A", "B", "C", "D", "E", "F", "G", "H", "I",
      "J", "K", "L", "M", "N", "O", "P", "Q", "R",
      "S", "T", "U", "V", "W", "X", "Y", "Z",
      "a", "b", "c", "d", "e", "f", "g", "h", "i",
      "j", "k", "l", "m", "n", "o", "p", "q", "r",
      "s", "t", "u", "v", "w", "x", "y", "z",
      "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "?", "=", "."
    ]
  };

  // JSON version of bok::bitmaps using objects instead of lists.
  // Each value is an object with a "type" string, an "offset" integer, and a
  // "width" integer.  "int" types can also have a "signed" boolean.
  // "str" types have a "count" integer in addition to "width", and can have
  // a "restrict" integer which corresponds to a table in 'ctables'.
  var bitmaps = {
    "jp": [ {}, {
      "region": {
        "type": "int", "offset": 0, "width": 3, "signed": false
      },
      "checksum": {
        "type": "int", "offset": 3, "width": 16, "signed": false
      },
      "offset": {
        "type": "int", "offset": 19, "width": 2, "signed": false
      },
      "sol/4": {
        "type": "int", "offset": 21, "width": 9, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 30, "width": 6, "signed": false
      },
      "hours": {
        "type": "int", "offset": 36, "width": 6, "signed": false
      },
      "minutes": {
        "type": "int", "offset": 42, "width": 6, "signed": false
      },
      "difficulty": {
        "type": "int", "offset": 48, "width": 3, "signed": false
      },
      "dungeons": {
        "type": "int", "offset": 51, "width": 5, "signed": false
      },
      "clears": {
        "type": "int", "offset": 56, "width": 3, "signed": false
      },
      "continues": {
        "type": "int", "offset": 59, "width": 4, "signed": false
      },
      "caught": {
        "type": "int", "offset": 63, "width": 5, "signed": false
      },
      "kills": {
        "type": "int", "offset": 68, "width": 7, "signed": false
      },
      "rank": {
        "type": "int", "offset": 75, "width": 4, "signed": false
      },
      "title": {
        "type": "int", "offset": 79, "width": 5, "signed": false
      },
      "name": {
        "type": "str", "offset": 84, "count": 5, "width": 40, "restrict": 0x180
      },
      "link-battles": {
        "type": "int", "offset": 124, "width": 6, "signed": false
      },
      "link-trades": {
        "type": "int", "offset": 130, "width": 6, "signed": false
      },
      "loan": {
        "type": "int", "offset": 136, "width": 8, "signed": false
      },
      "padding": {
        "type": "int", "offset": 144, "width": 0, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 24, "width": 6, "signed": false
      },
      "side": {
        "type": "int", "offset": 30, "width": 3, "signed": false
      },
      "style": {
        "type": "int", "offset": 33, "width": 4, "signed": false
      },
      "kills/8": {
        "type": "int", "offset": 37, "width": 8, "signed": false
      },
      "forges": {
        "type": "int", "offset": 45, "width": 7, "signed": false
      },
      "link-battles": {
        "type": "int", "offset": 52, "width": 6, "signed": false
      },
      "link-shopping" : {
        "type": "int", "offset": 58, "width": 6, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 64, "width": 10, "signed": true
      },
      "loan/256": {
        "type": "int", "offset": 74, "width": 8, "signed": true
      },
      "hours": {
        "type": "int", "offset": 82, "width": 6, "signed": false
      },
      "titles": {
        "type": "int", "offset": 88, "width": 16, "signed": false
      },
      "name": {
        "type": "str", "offset": 104, "count": 5, "width": 40, "restrict": 0x180
      },
      "padding": {
        "type": "int", "offset": 144, "width": 0, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 24, "width": 6, "signed": false},
      "kills/8": {
        "type": "int", "offset": 30, "width": 8, "signed": false
      },
      "forges": {
        "type": "int", "offset": 38, "width": 7, "signed": false
      },
      "races": {
        "type": "int", "offset": 45, "width": 9, "signed": false
      },
      "link-races": {
        "type": "int", "offset": 54, "width": 7, "signed": false
      },
      "cross-linked": {
        "type": "int", "offset": 61, "width": 1, "signed": false
      },
      "endings": {
        "type": "int", "offset": 62, "width": 6, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 68, "width": 10, "signed": false
      },
      "loan/256": {
        "type": "int", "offset": 78, "width": 8, "signed": false
      },
      "hours": {
        "type": "int", "offset": 86, "width": 6, "signed": false
      },
      "titles": {
        "type": "int", "offset": 92, "width": 12, "signed": false
      },
      "name": {
        "type": "str", "offset": 104, "count": 5, "width": 40, "restrict": 0x180
      },
      "padding": {
        "type": "int", "offset": 144, "width": 0, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "titles": {
        "type": "int", "offset": 24, "width": 14, "signed": false
      },
      "difficulty": {
        "type": "int", "offset": 38, "width": 2, "signed": false
      },
      "hours": {
        "type": "int", "offset": 40, "width": 7, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 47, "width": 15, "signed": false
      },
      "sword": {
        "type": "int", "offset": 62, "width": 3, "signed": false
      },
      "gun": {
        "type": "int", "offset": 65, "width": 3, "signed": false
      },
      "terrennial": {
        "type": "int", "offset": 68, "width": 3, "signed": false
      },
      "climate": {
        "type": "int", "offset": 71, "width": 3, "signed": false
      },
      "name-dark": {
        "type": "str", "offset": 74, "count": 10, "width": 80
      },
      "name-solar": {
        "type": "str", "offset": 154, "count": 10, "width": 80
      },
      "padding": {
        "type": "int", "offset": 234, "width": 6, "signed": false
      }
    }],
    "en": [ {}, {
      "region": {
        "type": "int", "offset": 0, "width": 3, "signed": false
      },
      "checksum": {
        "type": "int", "offset": 3, "width": 16, "signed": false
      },
      "offset": {
        "type": "int", "offset": 19, "width": 2, "signed": false
      },
      "sol/4": {
        "type": "int", "offset": 21, "width": 9, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 30, "width": 7, "signed": false
      },
      "hours": {
        "type": "int", "offset": 37, "width": 6, "signed": false
      },
      "minutes": {
        "type": "int", "offset": 43, "width": 6, "signed": false
      },
      "difficulty": {
        "type": "int", "offset": 49, "width": 3, "signed": false
      },
      "dungeons": {
        "type": "int", "offset": 52, "width": 5, "signed": false
      },
      "clears": {
        "type": "int", "offset": 57, "width": 3, "signed": false
      },
      "continues": {
        "type": "int", "offset": 60, "width": 4, "signed": false
      },
      "caught": {
        "type": "int", "offset": 64, "width": 5, "signed": false
      },
      "kills": {
        "type": "int", "offset": 69, "width": 7, "signed": false
      },
      "rank": {
        "type": "int", "offset": 76, "width": 4, "signed": false
      },
      "title": {
        "type": "int", "offset": 80, "width": 5, "signed": false
      },
      "name": {
        "type": "str", "offset": 85, "count": 9, "width": 72, "restrict": 0
      },
      "link-battles": {
        "type": "int", "offset": 157, "width": 6, "signed": false
      },
      "link-trades": {
        "type": "int", "offset": 163, "width": 6, "signed": false
      },
      "loan": {
        "type": "int", "offset": 169, "width": 8, "signed": false
      },
      "padding": {
        "type": "int", "offset": 177, "width": 15, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 24, "width": 7, "signed": false
      },
      "side": {
        "type": "int", "offset": 31, "width": 3, "signed": false
      },
      "style": {
        "type": "int", "offset": 34, "width": 4, "signed": false
      },
      "kills/8": {
        "type": "int", "offset": 38, "width": 8, "signed": false
      },
      "forges": {
        "type": "int", "offset": 46, "width": 7, "signed": false
      },
      "link-battles": {
        "type": "int", "offset": 53, "width": 6, "signed": false
      },
      "link-shopping": {
        "type": "int", "offset": 59, "width": 6, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 65, "width": 10, "signed": true
      },
      "loan/256": {
        "type": "int", "offset": 75, "width": 8, "signed": true
      },
      "hours": {
        "type": "int", "offset": 83, "width": 6, "signed": false
      },
      "titles": {
        "type": "int", "offset": 89, "width": 16, "signed": false
      },
      "name": {
        "type": "str", "offset": 105, "count": 9, "width": 72, "restrict": 0
      },
      "padding": {
        "type": "int", "offset": 177, "width": 15, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "timezone": {
        "type": "int", "offset": 24, "width": 7, "signed": false
      },
      "kills/8": {
        "type": "int", "offset": 31, "width": 8, "signed": false
      },
      "forges": {
        "type": "int", "offset": 39, "width": 7, "signed": false
      },
      "races": {
        "type": "int", "offset": 46, "width": 9, "signed": false
      },
      "link-races": {
        "type": "int", "offset": 55, "width": 7, "signed": false
      },
      "cross-linked": {
        "type": "int", "offset": 62, "width": 1, "signed": false
      },
      "endings": {
        "type": "int", "offset": 63, "width": 6, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 69, "width": 10, "signed": false
      },
      "loan/256": {
        "type": "int", "offset": 79, "width": 8, "signed": false
      },
      "hours": {
        "type": "int", "offset": 87, "width": 6, "signed": false
      },
      "titles": {
        "type": "int", "offset": 93, "width": 12, "signed": false
      },
      "name": {
        "type": "str", "offset": 105, "count": 9, "width": 72, "restrict": 0
      },
      "padding": {
        "type": "int", "offset": 177, "width": 15, "signed": false
      }
    }, {
      "checksum": {
        "type": "int", "offset": 0, "width": 16, "signed": false
      },
      "header-padding": {
        "type": "int", "offset": 16, "width": 2, "signed": false
      },
      "region": {
        "type": "int", "offset": 18, "width": 3, "signed": false
      },
      "offset": {
        "type": "int", "offset": 21, "width": 3, "signed": false
      },
      "titles": {
        "type": "int", "offset": 24, "width": 14, "signed": false
      },
      "difficulty": {
        "type": "int", "offset": 38, "width": 2, "signed": false
      },
      "hours": {
        "type": "int", "offset": 40, "width": 7, "signed": false
      },
      "sol/4096": {
        "type": "int", "offset": 47, "width": 15, "signed": false
      },
      "sword": {
        "type": "int", "offset": 62, "width": 3, "signed": false
      },
      "gun": {
        "type": "int", "offset": 65, "width": 3, "signed": false
      },
      "terrennial": {
        "type": "int", "offset": 68, "width": 3, "signed": false
      },
      "climate": {
        "type": "int", "offset": 71, "width": 3, "signed": false
      },
      "name-dark": {
        "type": "str", "offset": 74, "count": 10, "width": 80
      },
      "name-solar": {
        "type": "str", "offset": 154, "count": 10, "width": 80
      },
      "padding": {
        "type": "int", "offset": 234, "width": 6, "signed": false
      }
    }]
  };

  // JSON version of bok::ui::keyboards, 
  // Extra data is removed from the key lists and split between keyboards.alias
  // and keyboards.expand.  Empty strings still skip to the next column.
  var keyboards = {
  "en64": [
    ["B", "C", "D", "F", "G", "H", "J", "@", "#", "^", "⌫", "⌦"],
    ["K", "L", "M", "N", "P", "Q", "R", "0", "1", "2", "3", "4"],
    ["S", "T", "V", "W", "X", "Y", "Z", "5", "6", "7", "8", "9"],
    ["b", "c", "d", "f", "g", "h", "j", "+", "-", "/", "."     ],
    ["k", "l", "m", "n", "p", "q", "r", "=", "_", "?", "←", "→"],
    ["s", "t", "v", "w", "x", "y", "z", ":", ">", "!",    "␣"  ]
  ],
  "lk64": [
    ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", "⌫", "⌦"],
    ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"],
    ["N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"],
    ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"],
    ["n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"],
    ["?", "=",                     "␣",                     "←", "→"]
  ],
  "jp64": [
    ["あ", "い", "う", "え", "お", "ま", "み", "む", "め", "も", "⌫",  "⌦" ],
    ["か", "き", "く", "け", "こ", "や", "ゆ", "よ", "わ", "を", "ば", "び"],
    ["さ", "し", "す", "せ", "そ", "ら", "り", "る", "れ", "ろ", "ぶ", "べ"],
    ["た", "ち", "つ", "て", "と", "が", "ぎ", "ぐ", "げ", "ご", "ぼ"      ],
    ["な", "に", "ぬ", "ね", "の", "ざ", "じ", "ず", "ぜ", "ぞ", "←",  "→" ],
    ["は", "ひ", "ふ", "へ", "ほ", "だ", "ぢ", "づ", "で", "ど",    "␣"    ]
  ],
  "en": [
    ["A", "B", "C", "D", "E", "F", "G", "H", "I", "⌫", "⌦"],
    ["J", "K", "L", "M", "N", "O", "P", "Q", "R", ".", ","],
    ["S", "T", "U", "V", "W", "X", "Y", "Z", "",  "'", "-"],
    ["a", "b", "c", "d", "e", "f", "g", "h", "i", "/"     ],
    ["j", "k", "l", "m", "n", "o", "p", "q", "r", "←", "→"],
    ["s", "t", "u", "v", "w", "x", "y", "z", "",     "␣"  ]
  ],
  "ext": [
    ["&", "*", ";", "+", "-", "×", "÷", "=", "⌫", "⌦"],
    ["à", "á", "â", "ä", "è", "é", "ê", "ë", "(", ")"],
    ["ì", "í", "î", "ï", "ò", "ó", "ô", "ö", "[", "]"],
    ["ù", "ú", "û", "ü", "ç", "ñ", "œ", "ß", "!", "?"],
    ["À", "Á", "Â", "Ä", "È", "É", "Ê", "Ë", "¡", "¿"],
    ["Ì", "Í", "Î", "Ï", "Ò", "Ó", "Ô", "Ö", "←", "→"],
    ["Ù", "Ú", "Û", "Ü", "Ç", "Ñ", "Œ", "⋅",    "␣"  ]
  ],
  "hiri": [
    ["あ", "い", "う", "え", "お", "ら", "り", "る", "れ", "ろ", "⌫",  "⌦" ],
    ["か", "き", "く", "け", "こ", "が", "ぎ", "ぐ", "げ", "ご", "ん", "・"],
    ["さ", "し", "す", "せ", "そ", "ざ", "じ", "ず", "ぜ", "ぞ", "ー", "~" ],
    ["た", "ち", "つ", "て", "と", "だ", "ぢ", "づ", "で", "ど"            ],
    ["な", "に", "ぬ", "ね", "の", "ば", "び", "ぶ", "べ", "ぼ"            ],
    ["は", "ひ", "ふ", "へ", "ほ", "ぱ", "ぴ", "ぷ", "ぺ", "ぽ"            ],
    ["ま", "み", "む", "め", "も", "ぁ", "ぃ", "ぅ", "ぇ", "ぉ", "←",  "→" ],
    ["や", "ゆ", "よ", "わ", "を", "っ", "ゃ", "ゅ", "ょ", "",      "␣"    ]
  ],
  "kata": [
    ["ア", "イ", "ウ", "エ", "オ", "ラ", "リ", "ル", "レ", "ロ", "⌫",  "⌦" ],
    ["カ", "キ", "ク", "ケ", "コ", "ガ", "ギ", "グ", "ゲ", "ゴ", "ン", "・"],
    ["サ", "シ", "ス", "セ", "ソ", "ザ", "ジ", "ズ", "ゼ", "ゾ", "ー", "~" ],
    ["タ", "チ", "ツ", "テ", "ト", "ダ", "ヂ", "ヅ", "デ", "ド"            ],
    ["ナ", "ニ", "ヌ", "ネ", "ノ", "バ", "ビ", "ブ", "ベ", "ボ"            ],
    ["ハ", "ヒ", "フ", "ヘ", "ホ", "パ", "ピ", "プ", "ペ", "ポ"            ],
    ["マ", "ミ", "ム", "メ", "モ", "ァ", "ィ", "ゥ", "ェ", "ォ", "←",  "→" ],
    ["ヤ", "ユ", "ヨ", "ワ", "ヲ", "ッ", "ャ", "ュ", "ョ", "ヴ",    "␣"    ]
  ],
  "alias": {
    "⌫": "Backspace", "⌦": "Delete",
    "←": "ArrowLeft", "→": "ArrowRight",
    "␣": " "
  }, "expand": {
    "␣": true
  }};

  // 'decstr' converts a list of 8-bit integers to a string, mapping to
  // 'ctable'.  If 'table' is specified then the decoding only uses table
  // 'table', essentially assuming that each 8-bit integer is preceded by
  // the table-change character for 'table'.
  function decstr(list, table = "") {
    var i, r, f = "", len;
    if (typeof(table) == "number")
      table = "0x" + table.toString(16).toUpperCase();
    else if (typeof(table) == "undefined")
      table = ""
    if (table != "" && !(table in ctable))
      throw new Error("Invalid table: " + table);
    for (len = list.length; list[len - 1] == 0; len--) {}
    for (i = 0, r = []; i < len; i++) {
      if (f == "") {
        if (table == "") {
          f = "0x" + list[i].toString(16).toUpperCase();
          if (f in ctable)
            continue;
          f = "0x" + (list[i] >>> 8).toString(16).toUpperCase();
        } else
          f = table;
      }
      r.push(f in ctable && (list[i] & 0xFF) < ctable[f].length ?
          ctable[f][list[i] & 0xFF] : "�");
      f = "";
    }
    return r.join("");
  }

  // 'encstr' converts a string to a list of 8-bit integers, mapping from
  // 'ctable'.  If $tab is specified the encoder only uses the 'table' table,
  // essentially omitting the table-change characters.
  function encstr(str, table = "") {
    var i, r, k, t;
    if (typeof(table) == "number")
      table = "0x" + table.toString(16).toUpperCase();
    else if (typeof(table) == "undefined")
      table = "";
    if (table != "" && !(table in ctable))
      throw new Error("Invalid table: " + table);
    for (i = 0, r = []; i < str.length; i++) {
      if (str[i] == "�")
        break;
      if (table != "") {
        k = 0;
        t = ctable[table].indexOf(str[i]);
      } else {
        for (k in ctable)
          if ((t = ctable[k].indexOf(str[i])) >= 0)
            break;
        k = parseInt(k);
      }
      if (t < 0)
        break;
      if (k != 0)
        r.push(k);
      r.push(t);
    }
    return r;
  }

  // 'decb64' decodes the base64 string 'str' using 'b64table' into a list of
  // 6-bit integers, ignoring any [:space:] characters.  If the 'game' is 4 the
  // "jp" & "lk" tables are used, otherwise the "jp" & "en" tables are used.
  function decb64(str, game = 1) {
    var en = game < 4 ? "en" : "lk";
    var r = [];
    var i, t;
    for (i = 0; i < str.length; i++) {
      if (/\s/.test(str[i]))
        continue;
      if ((t = b64table.jp.indexOf(str[i])) < 0 &&
          (t = b64table[en].indexOf(str[i])) < 0)
        throw new Error("Invalid character: '" + str[i] + "'");
      if (t < 64)
        r.push(t);
    }
    return r;
  }

  // 'encb64' encodes a base64 string from 'list', which is list of 6-bit
  // integers using the appropriate alphabet for the 'game' / 'lang' pair.
  // 'lang' is set based on the region in 'list' if it is not specified.
  function encb64(list, game = 1, lang = "", pad = true) {
    var table, chars, pad, i, j;
    if (typeof(lang) == "undefined" || lang == "")
      lang = getlang(list);
    table = lang == "jp" ? "jp" : game < 4 ? "en" : "lk";
    pad = pad ? game >= 4 ? 5 : lang == "jp" ? 6 : 8 : 0;
    for (i = 0, j = 0, chars = []; i < list.length; i++) {
      if (list[i] < 0 || list[i] >= 64)
        throw new Error("Invalid value: " + list[i]);
      if (pad > 0 && j > 0 && j % pad == 0)
        chars.push(" ");
      chars.push(b64table[table][list[i]]);
      j++;
    }
    return chars.join("");
  }

  // 'getint' gets an integer of width 'count' at bit-offset 'offset' from the
  // list 'list' of 'width' wide integers, interpreting it as a two's complement
  // signed integer if 'signed' is true.
  // The bits are read and written LSb first.
  function getint(list, offset, count, signed = false, width = 6) {
    var i, v;
    count = Math.min(count, list.length * width - offset);
    for (v = 0, i = 0; i < count; i++, offset++)
      v |= (list[offset / width | 0] >>> offset % width & 1) << i;
    if (signed && count > 0 && (v >>> count - 1 & 1))
      v = -(~v + 1 & (1 << count) - 1);
    return v
  }

  // 'putint' puts the integer 'v' of width 'count' into list 'list' at
  // bit-offset 'offset' and returns the 'list'.  'list' is a list of 'width'
  // wide integers.  Zeros are added to the list as needed.  'v' is clamped to
  // [0,2^$count) for unsigned integers, and [-2^($count-1),2^($count-1)) for
  // signed integers.  Signed numbers are converted to two's-complement
  // formatted 'width' wide unsigned integers before writing.
  // The bits are written and read LSb first.
  function putint(list, v, offset, count, signed = false, width = 6) {
    var i, m;
    m = (1 << count - (signed && count > 0 ? 1 : 0)) - 1;
    v = signed ? Math.min(Math.max(v, -(m + 1)), m) + m + 1 ^ m + 1 :
      Math.min(Math.max(v, 0), m);
    for (i = 0; i < count; i++, offset++) {
      while ((offset / width | 0) >= list.length)
        list.push(0);
      list[offset / width | 0] &= ~(1 << offset % width);
      list[offset / width | 0] |= (v >>> i & 1) << offset % width;
    }
    return list
  }

  // 'getstr' gets the string converted from 'count' octets from the 'list' of
  // 'width' wide integers starting at bit-offset 'offset'.  'table' is passed
  // to 'decstr' along with the octets.
  function getstr(list, offset, count, table = "", width = 6) {
    var i, r;
    count = Math.min(count, list.length * width - offset >>> 3);
    for (i = 0, r = []; i < count; i++)
      r[i] = getint(list, offset + i * 8, 8, 0, width);
    return decstr(r, table);
  }

  // 'putstr' puts 'count' octets from the encoded 'str' into 'list' at bit-
  // offset 'offset'.  'list' is a list of 'width' wide integers and 'table'
  // is passed to 'encstr' along with $str.  'list' is then returned.
  function putstr(list, str, offset, count, table = "", width = 6) {
    var i, l = encstr(str, table);
    for (i = 0; i < count; i++)
      putint(list, i < l.length ? l[i] : 0, offset + i * 8, 8, 0, width);
    return list;
  }

  // 'forkeys' iterates over the provided keys, or array of keys, or all keys,
  // from the corresponding list for the 'game' / 'lang' pair in 'bitmaps'.
  // Values are read from the list 'list' of 'width' wide integers and the
  // callback is called with the key, value, and value from 'bitmaps'.
  // If 'FORKEYS_RESTRICT' is set in 'flags' then keys are skipped if not
  // present in 'list'.  If 'FORKEYS_MODIFY' is set in 'flags' then the value
  // in 'list' is set to the return value of the callback.
  var FORKEYS_RESTRICT = 1;
  var FORKEYS_MODIFY = 2;
  function forkeys(list, game, lang, width, flags /*, [key, ...] callback */) {
    var callback, args, map, i, k, v, f;
    if (arguments.length < 6)
      return new Error("Wrong number of arguments");
    if (typeof(lang) == "undefined" || lang == "")
      lang = getlang(list, game, width);
    map = bitmaps[lang][game];
    callback = arguments[arguments.length - 1];
    args = Array.prototype.slice.call(arguments, 5, arguments.length - 1);
    if (args.length == 1 && typeof(args[0]) == "object")
      args = args[0];
    if (args.length == 0)
      args = Object.keys(map);
    for (i = 0; i < args.length; i++) {
      k = args[i];
      if (!(k in map))
        continue;
      if ((flags & FORKEYS_RESTRICT) &&
          map[k].offset + map[k].width > list.length * width)
        continue;
      switch (map[k].type) {
      case "int":
        f = "signed" in map[k] ? map[k].signed : false;
        v = callback(k, getint(list, map[k].offset, map[k].width, f, width),
            map[k]);
        if (flags & FORKEYS_MODIFY)
          putint(list, v, map[k].offset, map[k].width, f, width);
        break;
      case "str":
        f = "restrict" in map[k] ? map[k].restrict : "";
        v = callback(k, getstr(list, map[k].offset, map[k].count, f, width),
            map[k]);
        if (flags & FORKEYS_MODIFY)
          putstr(list, v, map[k].offset, map[k].count, f, width);
        break;
      }
    }
    return list;
  }

  // 'getdict' returns a dictionary created by reading values from the 'list' of
  // 'width' wide integers for each key, or every key if none are given, as
  // specified in 'bitmaps' for the 'game' / 'lang' pair.
  function getdict(list, game = "", lang = "", width = 6, /*, [key ...] */) {
    var r = {};
    forkeys(list, game, lang, width, 0,
        Array.prototype.slice.call(arguments, 4), function (k, v) {r[k]=v});
    return r;
  }

  // 'getkeys' returns a list of values similar to 'getdict'.
  function getkeys(list, game = "", lang = "", width = 6 /*, key ... */) {
    var r = [];
    forkeys(list, game, lang, width, 0,
        Array.protoype.slice.call(arguments, 4), function (k, v) {
      r.push(v)
    });
    return r;
  }

  // 'getkeys' returns a single value similar to 'getkeys'
  function getkey(list, game, lang, width, key) {
    var r = undefined;
    forkeys(list, game, lang, width, 0, key, function (k, v) {r = v});
    return r;
  }

  // 'getregion' reads the region bits from the 'list' of 'width' wide integers
  // for game 'game' and returns a short region code, or an error.
  function getregion(list, game = 1, width = 6) {
    var r = getkey(list, game, "jp", width, "region");
    if (typeof(r) == "undefined" || r < 1 || r > 3)
      throw new Error("Invalid region: " + r);
    return ["", "jp", "na", "eu"][r];
  }

  // 'getlang' reads the region bits from the 'list' of 'width' wide integers
  // for game 'game' and returns a short language code, or an error.
  function getlang(list, game = 1, width = 6) {
    return {jp: "jp", na: "en", eu: "en"}[getregion(list, game, width)];
  }

  // 'whichgame' searches 'dict' for keys specific to a particular game, and
  // returns the game number of the first one found.
  function whichgame(dict) {
    var keys = [[
      "dungeons", "clears", "caught", "rank", "title", "link-trades",
      "rank-name", "title-name"
    ], [
      "side", "style", "link-shopping", "side-name", "style-name" 
    ], [
      "races", "link-races", "cross-linked", "endings"
    ], [
      "sword", "gun", "terrennial", "climate", "name-dark", "name-solar",
      "sword-name", "gun-name", "terrennial-name", "climate-name"
    ]];
    var i, j;
    for (i = 0; i < keys.length; i++)
      for (j = 0; j < keys[i].length; j++)
        if (keys[i][j] in dict)
          return i + 1;
    throw new Error("Unknown game");
  }

  // 'putdict' inserts the values from each key/value pair in 'dict' into
  // the 'list' of 'width' wide integers as specified in 'bitmaps' for the
  // 'game' / 'lang' pair and returns the modified 'list'
  function putdict(list, dict, game = "", lang = "", width = 6) {
    var args;
    if (typeof(game) == "undefined" || game == "")
      game = whichgame(dict);
    if (typeof(lang) == "undefined" || lang == "") {
      if (dict.region < 0 || dict.region > 3)
        throw new Error("Invalid region: " + dict.region);
      lang = ["", "jp", "en", "en"][dict.region];
    }
    return forkeys(list, game, lang, width, FORKEYS_MODIFY, Object.keys(dict),
        function (k) {return dict[k]});
  }

  // 'checksum' calculates the CRC-16 for the 'list' of 6-bit integers with an
  // initializer of 0xFFFF, the 'game' appropriate polynomial, and the output
  // inverted, and starting at the 'game' appropriate offset.
  function checksum(list, game = 1) {
    var j;
    var i = [4, 3, 3, 3][game - 1];
    var c = [0x1021, 0x1021, 0x8005, 0x180D][game - 1];
    var v = 0xFFFF;
    for (; i < list.length; i++) {
      v ^= list[i] << 8;
      for (j = 0; j < 8; j++)
        v = (v << 1 ^ ((v & 0x8000) ? c : 0)) & 0xFFFF;
    }
    return ~v & 0xFFFF;
  }

  // 'xor' encodes/decodes the password bits from the 'list' of 6-bit integers
  // the constant and the 'game' appropriate initalizer.
  // 'm' is the initialized value, 't' is initialized to ~0, and the offset is
  // read from the 'list'.  For each iteration 'm' is multiplied by the constant
  // then incremented by 1, and 't' is xored by the result.  After 'offset' plus
  // 4 iterations, starting at index 4 until the end of the $list, each $list
  // item is xored by the last 6 bits of $t before the next iteration.
  // The encoded list is returned.
  // Using the full constants causes the doubles to lose precision, luckily only
  // the last 6 bits of the actually matter, 'Math.imul' colud be used as well.
  function xor(list, game = 1) {
    //var m = [0x8C159, 0xB8E6E, 0x8C159, 0x5BB15][game - 1];
    var m = [0x19, 0x2E, 0x19, 0x15][game - 1];
    //var c = 0x6262C05D;
    var c = 0x1D;
    //var t = 0xFFFF;
    var t = 0x3F;
    var i = -getkey(list, game, "jp", 6, "offset");
    for (; i < list.length; i++) {
      if (i >= 4)
        list[i] ^= t;
      m = m * c + 1 & 0x3F;
      t ^= m;
    }
    return list;
  }

  // 'normalizelist' sets 'skey' in 'dict' based on the value of 'vkey' based on
  // the strings in 'list', or vice versa.  'name' is used for error messages
  // and defaults to 'vkey'.  If 'robust' is true then no errors are returned
  // for invalid values, or known invalid strings.
  function normalizelist(dict, list, vkey, skey, name = "", robust = false) {
    var invalid, unknown, v, s;
    if (typeof(name) == "undefined" || name == "")
      name = vkey;
    invalid = "Invalid " + name + " #";
    unknown = "Unknown " + name + " #";
    if (vkey in dict) {
      v = dict[vkey];
      s = v >= 0 && v < list.length ? list[v] : unknown + v;
      if (!robust && s == unknown + v)
        throw new Error(s);
      dict[skey] = s;
    } else if (skey in dict) {
      s = dict[skey];
      v = list.indexOf(s);
      if (v < 0 && s.indexOf(unknown) == 0) {
        v = parseInt(s.slice(unknown.length))
        if (!robust && (v < 0 || isNaN(v)))
          throw new Error(s);
      }
      if ((v < 0 || isNaN(v)) && s.indexOf(invalid) == 0) {
        v = parseInt(s.slice(invalid.length))
        if (!robust && (v < 0 || isNaN(v)))
          throw new Error(s);
      }
      if ((v < 0 || isNaN(v)) && (s != "" || !robust))
        throw new Error("Invalid " + skey + ": " + s);
      dict[vkey] = v;
    }
    return dict;
  }

  // 'normalizebits' sets 'lkey' in 'dict' based on the value of 'vkey' based on
  // the strings in 'list', or vice versa.  'name' is used for error messages
  // and defaults to 'vkey'.  If 'robust' is true then invalid values and items
  // are ignored.
  function normalizebits(dict, list, vkey, lkey, name = "", robust = false) {
    var v, l, i, j;
    if (typeof(name) == "undefined" || name == "")
      name = vkey;
    if (vkey in dict) {
      v = dict[vkey];
      if (typeof(v) != "number")
        throw new Error("Invalid " + name + ": " + v);
      for (i = 0, l = []; i < list.length; i++)
        if (v & 1 << i) {
          v &= ~(1 << i);
          l.push(list[i]);
        }
      if (!robust && v != 0) {
        for (; !(v & 1 << i); i++) {}
        throw new Error("Unknown " + name + " #" + i);
      }
      dict[lkey] = l;
    } else if (lkey in dict) {
      l = dict[lkey];
      for (i = 0, v = 0; i < l.length; i++) {
        j = list.indexOf(l[i]);
        if (!robust && i < 0)
          throw new Error("Unknown " + name + ": " + l[i]);
        v |= i >= 0 ? 1 << i : 0;
      }
      dict[vkey] = v;
    }
    return dict;
  }

  // 'normalize' normalizes the provided dictionary 'dict' using 'locale'
  // strings such that if "sol/4096" or "timezone-name" exists then "sol" and
  // "timezone" will be set to the equivalent values.  If 'robust' is set then
  // no ignorable errors are returned.
  function normalize(dict, locale = "en", robust = false) {
    var region, lang;
    var game = whichgame(dict);
    normalizelist(dict, strings[locale].regions, "region", "region-name",
      strings[locale].labels.region, robust);
    region = "region" in dict ? dict.region : 0;
    if (!robust && (region < 1 || region > 3))
      throw new Error("Unknown region");
    lang = ["??", "jp", "en", "en", "??", "??", "??", "??"][region];
    region = ["??", "jp", "na", "eu", "??", "??", "??", "??"][region];
    dict.game = game;
    dict["game-name"] = strings[locale].games[lang + game];
    // Zeros
    ["header-padding", "padding", "offset"].forEach(function (k) {
      if (!(k in dict))
        dict[k] = 0;
    });
    // Sol
    if ("sol" in dict) {
      dict["sol/4"] = dict.sol >>> 2;
      dict["sol/4096"] = dict.sol >>> 12;
    } else if ("sol/4" in dict) {
      dict.sol = dict["sol/4"] << 2;
      dict["sol/4096"] = dict["sol/4"] >>> 10;
    } else if ("sol/4096" in dict) {
      dict.sol = dict["sol/4096"] << 12;
      dict["sol/4"] = dict["sol/4096"] << 10;
    }
    // Kills
    if ("kills" in dict)
      dict["kills/8"] = dict.kills >>> 3;
    else if ("kills/8" in dict)
      dict.kills = dict["kills/8"] << 3;
    // Loans
    if ("loan" in dict)
      dict["loan/256"] = dict.loan >>> 8;
    else if ("loan/256" in dict)
      dict.loan = dict["loan/256"] << 8;
    // Timezones
    if (region in strings[locale].timezones)
      normalizelist(dict, strings[locale].timezones[region], "timezone",
          "timezone-name", strings[locale].labels.timezone, robust);
    // Boktai 1
    normalizelist(dict, strings[locale].ranks, "rank", "rank-name",
        strings[locale].labels.rank, robust);
    normalizelist(dict, strings[locale].titles[1], "title", "title-name",
        strings[locale].labels.title, robust);
    normalizelist(dict, strings[locale].difficulties[game], "difficulty",
        "difficulty-name", strings[locale].labels.difficulty, robust);
    // Boktai 2
    normalizelist(dict, strings[locale].sides, "side", "side-name",
        strings[locale].labels.side, robust);
    normalizelist(dict, strings[locale].styles, "style", "style-name",
        strings[locale].labels.style, robust);
    // Titles
    normalizebits(dict, strings[locale].titles[game], "titles", "titles-list",
        strings[locale].labels.titles, robust);
    // Endings
    normalizebits(dict, strings[locale].endings, "endings", "ending-list",
        strings[locale].labels.endings, robust);
    // Favorites
    normalizelist(dict, strings[locale].swords, "sword", "sword-name",
        strings[locale].labels.swords, robust);
    normalizelist(dict, strings[locale].guns, "gun", "gun-name",
        strings[locale].labels.guns, robust);
    normalizelist(dict, strings[locale].terrennials, "terrennial",
        "terrennial-name", strings[locale].labels.terrennials, robust);
    normalizelist(dict, strings[locale].climates, "climate",
        "climate-name", strings[locale].labels.swords, robust);
    return (dict);
  }

  // 'parseregion' returns the region number for the region speified by 'str',
  // or an error if it is unmatched.
  function parseregion(str) {
    if (/^(japan|jp)$/i.test(str))
      return 1;
    if (/^((north.?)?america|na|united.?states|usa?|english)$/i.test(str))
      return 2;
    if (/^(europe|eu)$/i.test(str))
      return 3;
    throw new Error("Unknown region: " + region);
  }

  // 'decpass' decodes the base64 string 'pass' into a dictionary.
  // 'normalize'ing it if 'normlocale' is provided'
  function decpass(pass, game = 1, lang = "", normlocale = "") {
    var len, sum, dict;
    var l = decb64(pass, game);
    if (typeof(lang) == "undefined" || lang == "")
      lang = getlang(l, game);
    len = {jp: [24, 24, 24, 40], en: [32, 32, 32, 40]}[lang][game-1];
    if (l.length != len)
      throw new Error("Invalid length: " + l.length + " != " + len);
    sum = checksum(l, game);
    dict = getdict(xor(l, game), game, lang);
    if (dict.checksum != sum)
      dict.checksum = dict.checksum + "!=" + sum
    if (typeof(normlocale) == "string" && normlocale in strings)
      normalize(dict, normlocale, true);
    return dict;
  }

  // 'encpass' encodes 'dict' into a base64 password.
  // 'normalize'ing it if 'normlocale' is provided'
  function encpass(dict, game = "", lang = "", normlocale = "") {
    var l;
    if (typeof(normlocale) == "string" && normlocale in strings)
      normalize(dict, normlocale, true);
    if (typeof(lang) == "undefined" || lang == "")
      lang = dict.region == 1 ? "jp" : "en";
    if (typeof(game) == "undefined" || game == "")
      game = whichgame(dict);
    l = xor(putdict([], dict, game, lang), game);
    putdict(l, {checksum: checksum(l, game)}, game, lang);
    return encb64(l, game, lang);
  }

  // 'getuiinfo' returns a list of ui element types, values, and defaults
  // depending on the values of 'game', 'region', and 'locale'.
  // Each value is an object with a "name" string and a "type" string, and can
  // have a "default" value.  "list" types have a "values" array, "regionlist"
  // types have a "values" object containing arrays for each short region code.
  // "int" types have a "min" integer, a "max" integer, and can also have a
  // "step" integer.  "string" types have a "max" array or region-keyed object,
  // can have a "max-bytes" array or region-keyed object, and can have a
  // "restrict" boolean.  "bits" types have a "values" array.  Empty names or
  // "break" types will will ensure the next input is at the start of a row.
  var uiinfo = {};
  function getuiinfo(game, region = "na", locale = "en") {
    var regionnum, timezones;
    if (!(locale in strings))
      throw new Error("Unknown locale: " + locale);
    regionnum = parseregion(region);
    region = ["", "jp", "na", "eu"][regionnum]
    if (!(locale in uiinfo))
      uiinfo[locale] = {};
    if (!(region in uiinfo[locale]))
      uiinfo[locale][region] = {};
    if (game in uiinfo[locale][region])
      return uiinfo[locale][region][game];
    timezones = {};
    Object.keys(strings[locale].timezones).forEach(function (key) {
      timezones[key] = strings[locale].timezones[key].slice(1);
    });
    switch (game) {
    case 1:
      return uiinfo[locale][region][game] = [
        { name: "region-name", type: "list",
          values: strings[locale].regions.slice(1),
          default: regionnum - 1
        },
        { name: "offset", type: "int", min: 0, max: 3 },
        { name: "sol", type: "int", min: 0, max: 0x7FC, step: 4 },
        { name: "timezone-name", type: "regionlist",
          values: timezones, default: { jp: 5, na: 14, eu: 21 }
        },
        { name: "", type: "break" },
        { name: "hours", type: "int", min: 0, max: 63 },
        { name: "minutes", type: "int", min: 0, max: 59, default: 1 },
        { name: "difficulty-name", type: "list",
          values: strings[locale].difficulties[1],
          default: 2
        },
        { name: "dungeons", type: "int", min: 0, max: 29 },
        { name: "clears", type: "int", min: 1, max: 7 },
        { name: "continues", type: "int", min: 0, max: 15 },
        { name: "caught", type: "int", min: 0, max: 31 },
        { name: "kills", type: "int", min: 0, max: 127 },
        { name: "rank-name", type: "list", values: strings[locale].ranks },
        { name: "title-name", type: "list", values: strings[locale].titles[1] },
        { name: "name", type: "string",
          max: { jp: 5, na: 9, eu: 9 },
          default: { jp: "ジャンゴ", na: "Django", eu: "Django" },
          restrict: true
        },
        { name: "link-battles", type: "int", min: 0, max: 63 },
        { name: "link-trades", type: "int", min: 0, max: 63 },
        { name: "loan", type: "int", min: 0, max: 255 },
        { name: "padding", type: "hidden", default: 0 }
      ];
    case 2:
      return uiinfo[locale][region][game] = [
        { name: "region-name", type: "list",
          values: strings[locale].regions.slice(1),
          default: regionnum - 1
        },
        { name: "offset", type: "int", min: 0, max: 7 },
        { name: "timezone-name", type: "regionlist",
          values: timezones, default: { jp: 5, na: 14, eu: 21 }
        },
        { name: "side-name", type: "list", values: strings[locale].sides },
        { name: "style-name", type: "list", values: strings[locale].styles },
        { name: "kills", type: "int", min: 0, max: 0x7F8, step: 8 },
        { name: "forges", type: "int", min: 0, max: 127 },
        { name: "link-battles", type: "int", min: 0, max: 63 },
        { name: "link-shopping", type: "int", min: 0, max: 63 },
        { name: "sol", type: "int", min: 0, max: 0x1FF000, step: 0x1000 },
        { name: "loan", type: "int", min: 0, max: 0x7F00, step: 0x100 },
        { name: "hours", type: "int", min: 0, max: 63 },
        { name: "name", type: "string",
          max: { jp: 5, na: 9, eu: 9 },
          default: { jp: "ジャンゴ", na: "Django", eu: "Django" },
          restrict: true
        },
        { name: "titles", type: "bits",
          values: strings[locale].titles[2], default: 0x40
        },
        { name: "header-padding", type: "hidden", default: 0 },
        { name: "padding", type: "hidden", default: 0 }
      ];
    case 3:
      return uiinfo[locale][region][game] = [
        { name: "region-name", type: "list",
          values: strings[locale].regions.slice(1, 2),
        },
        { name: "offset", type: "int", min: 0, max: 7 },
        { name: "timezone-name", type: "regionlist",
          values: timezones, default: { jp: 5, na: 14, eu: 21 }
        },
        { name: "kills", type: "int", min: 0, max: 0x7F8, step: 8 },
        { name: "forges", type: "int", min: 0, max: 127 },
        { name: "races", type: "int", min: 0, max: 511 },
        { name: "link-races", type: "int", min: 0, max: 127 },
        { name: "cross-linked", type: "boolean" },
        { name: "sol", type: "int", min: 0, max: 0x3FF000, step: 0x1000 },
        { name: "loan", type: "int", min: 0, max: 0xFF00, step: 0x100 },
        { name: "hours", type: "int", min: 0, max: 63 },
        { name: "name", type: "string",
          max: { jp: 5, na: 9, eu: 9 },
          default: { jp: "ジャンゴ", na: "Django", eu: "Django" },
          restrict: true
        },
        { name: "endings",
          type: "bits", values: strings[locale].endings, default: 1 },
        { name: "titles", type: "bits", values: strings[locale].titles[3] },
        { name: "header-padding", type: "hidden", default: 0 },
        { name: "padding", type: "hidden", default: 0 }
      ];
    case 4:
      return uiinfo[locale][region][game] = [
        { name: "region-name", type: "list",
          values: strings[locale].regions.slice(1),
          default: regionnum - 1
        },
        { name: "offset", type: "int", min: 0, max: 7 },
        { name: "difficulty-name",
          type: "list", values: strings[locale].difficulties[4] },
        { name: "hours", type: "int", min: 0, max: 127 },
        { name: "sol", type: "int", min: 0, max: 0x7FFF000, step: 0x1000 },
        { name: "", type: "break" },
        { name: "sword-name", type: "list", values: strings[locale].swords },
        { name: "gun-name", type: "list", values: strings[locale].guns },
        { name: "terrennial-name",
          type: "list", values: strings[locale].terrennials },
        { name: "climate-name",
          type: "list", values: strings[locale].climates },
        { name: "", type: "break" },
        { name: "name-dark", type: "string",
          max: { jp: 5, na: 10, eu: 10 },
          "max-bytes": { jp: 10, na: 10, eu: 10 },
          default: { jp: "サバタ", na: "Lucian", eu: "Lucian" }
        },
        { name: "name-solar", type: "string",
          max: { jp: 5, na: 10, eu: 10 },
          "max-bytes": { jp: 10, na: 10, eu: 10 },
          default: { jp: "ジャンゴ", na: "Aaron", eu: "Aaron" }
        },
        { name: "titles", type: "bits", values: strings[locale].titles[4] },
        { name: "header-padding", type: "hidden", default: 0 },
        { name: "padding", type: "hidden", default: 0 }
      ];
    default:
      throw new Error("Unknown game: " + game);
    }
  }

  // 'uifindname' returns the first child element of 'root' with name 'name'.
  function uifindname(root, name) {
    var i, r;
    if ("name" in root && root.name == name)
      return root;
    for (i = 0, r = undefined;
        typeof(r) == "undefined" && i < root.children.length;
        r = uifindname(root.children[i++], name)) {}
    return r;
  }

  // 'uiregion' returns the region from 'uifindname(root, "region-name")' for
  // 'locale' as an object with the "string", a "number", and a short "code".
  function uiregion(root, locale = "en") {
    var dict = {};
    dict.string = uifindname(root, "region-name").value;
    normalizelist(dict, strings[locale].regions, "number", "string",
        strings[locale].labels.region, true);
    dict.code = ["??", "jp", "na", "eu", "??", "??", "??", "??"][dict.number];
    return dict;
  }

  // 'uivalidatestring' validates the string value of 'input' in 'root' with
  // 'info' from 'getuiinfo', the 'locale', and the event 'type'.  The character
  // values are restricted to those in 'ctable'.  'info.max' restricts the
  // character length of the value, and defaults to 'Infinity';
  // 'info["max-bytes"]' restricts the byte length of the value, and defaults
  // to 'info.max'.  If 'info.restrict' is true then the table is restricted to
  // 0x180 if the region is set to Japan or 0x0 otherwise.  If the value is empty
  // and the event 'type' is not "input" then it is set to the default string.
  // 'info.default', 'info.max', and 'info["max-bytes"]' can all be region
  // specific if they are objects with short region codes as keys.
  function uivalidatestring(root, input, info, locale = "en", type = "") {
    var s, l, f, carat;
    var region = uiregion(root, locale).code;
    var max = "max" in info ? info.max : Infinity;
    var bmax = "max-bytes" in info ? info["max-bytes"] : max;
    var def = "default" in info ? info.default : max;
    max = typeof(max) == "object" && region in max ? max[region] : max;
    bmax = typeof(bmax) == "object" && region in bmax ?  bmax[region] : bmax;
    def = typeof(def) == "object" && region in def ?  def[region] : def;
    carat = input.selectionStart;
    s = input.value.slice(0, max);
    f = "restrict" in info && info.restrict ? (region == "jp" ? 0x180 : 0) : "";
    l = encstr(s, f);
    if (l.length > bmax)
      l = l.slice(0, bmax);
    s = decstr(l, f);
    if (type != "input" && s.length == 0)
      s = def;
    if (s != input.value) {
      input.value = s;
      input.setSelectionRange(carat, carat);
    }
  }

  // 'uivalidateinteger' validates the string value of 'input' in 'root' with
  // 'info' from 'getuiinfo', the 'locale', and the event 'type'.  The value is
  // restricted to numbers from 'info.min' to 'info.max', and if the value is
  // not a number and 'type' is not "input" then it is set to 'info.default',
  // which defaults to 'info.min'.  'info.default' can be region specific if
  // it is an object with short region codes as keys.
  function uivalidateinteger(root, input, info, locale = "en", type = "") {
    var i;
    var region = uiregion(root, locale).code;
    var def = "default" in info ? info.default : info.min;
    def = typeof(def) == "object" && region in def ? def[region] : def;
    if (input.value == "" && type == "input")
      return;
    i = parseInt(input.value);
    if (isNaN(i) && type == "input")
      return;
    if (isNaN(i))
      input.value = def;
    else if (i < info.min)
      input.value = info.min;
    else if (i > info.max)
      input.value = info.max;
  }

  // 'updateregion' updates any region dependant entries/lists for 'game' in
  // 'root' based on the value of 'uiregion' for 'locale'.
  function uiupdateregion(root, game, locale) {
    var dict, info, lang, cur, t, l, i, j, def;
    dict = uiregion(root, locale);
    lang = dict.code == "jp" ? "jp" : "en";
    cur = uifindname(root, "password");
    cur.value = encb64(forkeys(decb64(cur.value, game), game, lang, 6,
        FORKEYS_RESTRICT | FORKEYS_MODIFY, "region", function (k, v) {
      return dict.number;
    }), game, lang);
    info = getuiinfo(game, dict.code, locale);
    for (i = 0; i < info.length; i++)
      switch (info[i].type) {
      case "string":
        uivalidatestring(root, uifindname(root, info[i].name), info[i], locale);
        break;
      case "regionlist":
        def = "default" in info[i] ? info[i].default : 0;
        if (typeof(def) == "object" && dict.code in def)
          def = def[dict.code];
        cur = uifindname(root, info[i].name);
        while (cur.children.length > 0)
          cur.removeChild(cur.children[0]);
        l = info[i].values;
        l = dict.code in l ? l[dict.code] : l;
        l = typeof(l) == "undefiend" || !("length" in l) ? [] : l;
        for (j = 0; j < l.length; j++) {
          cur.appendChild(t = document.createElement("option"));
          t.selected = j == def;
          t.append(l[j]);
        }
        break;
      }
    return dict.code;
  }

  // 'uicycleoffset' increments the offset bit in the current password in
  // 'uifindname(root, "password")' for 'game' by 1.
  // The language to use is derived from 'uiregion' for 'root' and 'locale'.
  function uicycleoffset(root, game, locale = "en") {
    var input = uifindname(root, "password");
    var region = uiregion(root, locale).code;
    input.value = encb64(forkeys(decb64(input.value, game), game,
        region == "jp" ? "jp" : "en", 6, FORKEYS_RESTRICT | FORKEYS_MODIFY,
        "offset", function (k, v, i) {
      return v + 1 & (1 << i.width) - 1;
    }), game, region == "jp" ? "jp" : "en");
  }

  // 'uiencode' encodes the input values in 'root' for 'game' with 'prefix' and
  // 'locale' into the password entry.
  function uiencode(root, game, locale = "en", prefix = "bokpass") {
    var region = uiregion(root, locale).code;
    var lang = region == "jp" ? "jp" : "en";
    var info = getuiinfo(game, region, locale);
    var dict, cur, i, j, t, v;
    for (i = 0, dict = {}; i < info.length; i++) {
      if (info[i].name == "" || info[i].type == "break")
        continue;
      if (info[i].type != "bits" &&
          typeof(cur = uifindname(root, info[i].name)) == "undefined")
        throw new Error("No input for: " + info[i].name);
      switch (info[i].type) {
      case "boolean":
        v = cur.checked ? 1 : 0;
        break;
      case "bits":
        for (v = 0, j = 0; typeof(cur = uifindname(
            root, info[i].name + "-" + j)) != "undefined";
            v |= cur.checked ? 1 << j : 0, j++) {}
        break;
      default:
        v = cur.value;
        break;
      }
      dict[info[i].name] = v;
    }
    cur = root.getElementsByClassName(prefix + "-errors")[0];
    cur.innerText = "";
    try {
      normalize(dict, locale);
    } catch (e) {
      cur.innerText = "⚠ " + e;
    } finally {
      switch (game) {
      case 1:
        if (dict.minutes == 0 && dict.hours == 0)
          cur.innerText = "⚠ " + strings[locale].labels.minutes + " or " +
              strings[locale].labels.hours + " should be set"
        else if (dict.minutes >= 60)
          cur.innerText =
              "⚠ " + strings[locale].labels.minutes + " should be less than 60";
        else if (dict.clears == 0)
          cur.innerText =
              "⚠ " + strings[locale].labels.clears + " should be set";
        else if (dict["name"] == "")
          cur.innerText = "⚠ " + strings[locale].labels.name + " should be set";
        break;
      case 2:
        if ((dict.titles & 1 << 6) == 0)
          cur.innerText =
              "⚠ " + strings[locale].titles[2][6] + " should be set";
        else if (dict["name"] == "")
          cur.innerText = "⚠ " + strings[locale].labels.name + " should be set";
        break;
      case 3:
        if (dict["name"] == "")
          cur.innerText = "⚠ " + strings[locale].labels.name + " should be set";
        break;
      case 4:
        if (dict["name-dark"] == "")
          cur.innerText =
              "⚠ " + strings[locale].labels["name-dark"] + " should be set";
        else if (dict["name-solar"] == "")
          cur.innerText =
              "⚠ " + strings[locale].labels["name-solar"] + " should be set";
        break;
      }
    }
    uifindname(root, "password").value = encpass(dict, game, lang, locale)
  }

  // 'uidecode' decodes the current password into the input values in 'root' for
  // 'game' with 'prefix' and 'locale'.
  function uidecode(root, game, locale = "en", prefix = "bokpass") {
    var region = uiregion(root, locale).code;
    var lang = region == "jp" ? "jp" : "en";
    var info = getuiinfo(game, region, locale);
    var cur = root.getElementsByClassName(prefix + "-errors")[0];
    cur.innerText = "";
    try {
      var dict = decpass(uifindname(root, "password").value, game, lang,
        locale);
    } catch (e) {
      cur.innerText = "⚠ " + e;
      return;
    } finally {
      if (typeof(dict.checksum) == "string" && dict.checksum.indexOf("!=") >= 0)
        cur.innerText = "⚠ checksum " + dict.checksum;
    }
    var cur, i, j, t, v;
    for (i = 0; i < info.length; i++) {
      if (info[i].name == "" || info[i].type == "break")
        continue;
      if (!(info[i].name in dict))
        throw new Error("No value for: " + info[i].name);
      v = dict[info[i].name];
      if (info[i].type != "bits")
        cur = uifindname(root, info[i].name);
      switch (info[i].type) {
      case "boolean":
        cur.checked = v;
        break;
      case "bits":
        for (j = 0; typeof(cur = uifindname(root, info[i].name + "-" + j)) !=
            "undefined"; j++)
          cur.checked = !!(v & 1 << j);
        break;
      case "regionlist":
      case "list":
        cur.value = v;
        if (cur.value != v) {
          cur.appendChild(t = document.createElement("option"));
          t.selected = true;
          t.append(v);
          cur.value = v;
          cur.dispatchEvent(new Event("change", {
            bubbles: true, cancelable: true
          }));
          cur.addEventListener("change", (function (cur, t) {
            return function (e) {
              cur.removeChild(t);
            }
          })(cur, t), { once: true });
        }
        break;
      default:
        cur.value = v;
        break;
      }
    }
  }

  // 'uiallowkeyboards' disables/enables all keyboard tabs for 'prefix'
  // depending on the arguments given, defaulting to enabling all keyboards.
  // If one is set to a keyboard not specified then the tab is changed to the
  // first keyboard provided.
  function uiallowkeyboards(prefix, /*, [kbd ...] */) {
    var kbds = document.getElementsByClassName(prefix + "-keyboards");
    var args = Array.prototype.slice.call(arguments, 1);
    var kbd, i, j;
    if (args.length == 1 && typeof(args[0]) == "object")
      args = args[0];
    if (args.length == 0)
      for (i = 0; i < strings.en.labels["keyboard-tabs"].length; i++)
        args.push(strings.en.labels["keyboard-tabs"][i].key);
    for (i = 0; i < kbds.length; i++) {
      kbd = uifindname(kbds[i], "keyboard-tabs");
      if (typeof(kbd) == "undefined")
        continue;
      if (args.indexOf(kbd.value) < 0) {
        kbd.value = args[0];
        kbd.dispatchEvent(new Event("change", {
          bubbles: true, cancelable: true
        }));
      }
      for (j = 0; j < kbd.length; j++) {
        kbd[j].disabled = args.indexOf(kbd[j].value) < 0;
        kbd[j].selected = kbd[j].value == kbd.value;
      }
    }
  }

  // 'uicreate' initializes a grid of inputs based on the values from
  // 'getuiinfo(game, region, locale)' using 'prefix' for classes and ids
  // returning a "div" of class "'prefix'-game" containing them.
  // The number of columns can be specified with 'options.columns', and
  // 'options.nostyle' will disable the automatic grid styling entirely.
  // 'options.invalid' will disable calling 'uivalidateinteger' and
  // 'uivalidatestring' on "input" and "focusout" events, 'uivalidatestring'
  // will still be called when the region is changed.
  // If 'columns' is unset or zero it tries to guess the amount of columns to
  // use for the automatic grid styling from the window dimentions.
  // To automatically reflow the grid on resize use 'options.nostyle' and CSS
  // with "@media" queries.
  // Important classes are:
  //   "'prefix'-game" for the main "div"
  //   "'prefix'-password" for the password "div"
  //   "'prefix'-game-grid" for the input grid
  //   "'prefix'-clear" for elements that should start a row
  //   "'prefix'-errors" for the error "p"
  //   "'prefix'-string" for string inputs
  //   "'prefix'-int" for number inputs
  //   "'prefix'-list" for list inputs
  //   "'prefix'-boolean" for boolean inputs
  //   "'prefix'-bits" for bit-flag inputs
  //   "'prefix'-'name'" for individual inputs, where name is from 'getuiinfo'
  function uicreate(region, game, prefix = "bokpass", locale = "en",
        options = {}) {
    var root, container, label, cur, t;
    var i, j, cb, info, def, l, clear = false;
    if (!("columns" in options && typeof(options.columns) == "number" &&
        options.columns >= 1)) {
      if ("self" in window && "matchMedia" in self)
        for (options.columns = 1; options.columns < 4 && self.matchMedia(
            "(min-width: " + (24 * (options.columns + 1)) + "em)").matches;
            options.columns++) {}
      else
        options.columns = Math.min(Math.floor(
            window.innerWidth / window.devicePixelRatio / 360), 4);
    }
    info = getuiinfo(game, region, locale);
    root = document.createElement("div");
    root.className = prefix + "-game";
    root.appendChild(container = document.createElement("div"));
    container.className = prefix + "-password";
    if (!("nostyle" in options && options.nostyle)) {
      container.style.width = "100%";
      container.style.gridColumn = "span " + options.columns * 2;
      container.style.display = "flex";
      container.style.flexDirection = "row";
    }
    container.appendChild(cur = document.createElement("input"));
    cur.type = "button";
    cur.name = "encode";
    cur.value = "↱";
    if (!("nostyle" in options && options.nostyle)) {
      cur.style.font = "large mono";
      cur.style.minHeight = "1.5em";
      cur.style.aspectRatio = 1;
    }
    cur.addEventListener("click", function () {
      uiencode(root, game, locale, prefix);
    });
    container.appendChild(cur = document.createElement("input"));
    cur.type = "text";
    cur.name = "password";
    cur.value = "";
    if (!("nostyle" in options && options.nostyle)) {
      cur.style.font = "large mono";
      cur.style.flexGrow = 1;
    }
    cur.addEventListener("focusin", function (e) {
      var r = root.parentElement.parentElement;
      if (uiregion(root, locale).code == "jp")
        uiallowkeyboards(prefix, "jp64");
      else if (game == 4)
        uiallowkeyboards(prefix, "lk64");
      else
        uiallowkeyboards(prefix, "en64");
    });
    cur.addEventListener("focusout", function (e) {
      uiallowkeyboards(prefix);
    });
    container.appendChild(cur = document.createElement("input"));
    cur.type = "button";
    cur.name = "cycleoffset";
    cur.value = "↻";
    if (!("nostyle" in options && options.nostyle)) {
      cur.style.font = "large mono";
      cur.style.minHeight = "1.5em";
      cur.style.aspectRatio = 1;
    }
    cur.addEventListener("click", function () {
      uicycleoffset(root, game, locale);
    });
    container.appendChild(cur = document.createElement("input"));
    cur.type = "button";
    cur.name = "decode";
    cur.value = "↴";
    if (!("nostyle" in options && options.nostyle)) {
      cur.style.font = "large mono";
      cur.style.minHeight = "1.5em";
      cur.style.aspectRatio = 1;
    }
    cur.addEventListener("click", function () {
      uidecode(root, game, locale, prefix);
    });
    root.appendChild(container = document.createElement("div"));
    container.className = prefix + "-game-grid";
    if (!("nostyle" in options && options.nostyle)) {
      container.style.display = "grid";
      container.style.gridTemplateColumns =
          "repeat(" + options.columns + ", auto 1fr)";
    }
    cb = {
      string: function (info) {
        return function (e) {
          var r = root.parentElement.parentElement;
          switch (e.type) {
          case "focusin":
            if (uiregion(container, locale).code == "jp") {
              if (game == 4)
                uiallowkeyboards(prefix, "hiri", "kata", "en", "ext");
              else
                uiallowkeyboards(prefix, "hiri", "kata");
            } else if (game == 4)
              uiallowkeyboards(prefix, "en", "ext", "hiri", "kata");
            else
              uiallowkeyboards(prefix, "en");
            break;
          default:
            if ("invalid" in options && options.invalid)
              uivalidatestring(container, e.target, info, locale, e.type);
            break;
          }
        };
      },
      int: function (info) {
        return function (e) {
          if ("invalid" in options && options.invalid)
            uivalidateinteger(container, e.target, info, locale, e.type);
        };
      }
    };
    for (i = 0; i < info.length; i++) {
      if (info[i].name == "" || info[i].type == "break") {
        clear = true;
        continue;
      }
      if (info[i].type != "hidden") {
        container.appendChild(label = document.createElement("label"));
        if (clear || info[i].type == "bits") {
          label.className = prefix + "-clear";
          if (!("nostyle" in options && options.nostyle))
            label.style.gridColumnStart = 1;
          clear = false;
        }
        label.append(strings[locale].labels[info[i].name] + ":");
        if (!("nostyle" in options && options.nostyle))
          label.style.textAlign = "right";
      }
      def = "default" in info[i] ? info[i].default : undefined;
      def = typeof(def) == "object" && region in def ? def[region] : def;
      switch (info[i].type) { 
      case "string":
        container.appendChild(cur = document.createElement("input"));
        label.htmlFor = cur.id = prefix + "-" + info[i].name;
        cur.className = prefix + "-string " + prefix + "-" + info[i].name;
        cur.type = "text";
        cur.name = info[i].name;
        t = cb[info[i].type](info[i]);
        cur.addEventListener("focusin", t);
        cur.addEventListener("focusout", t);
        cur.addEventListener("input", t);
        break;
      case "int":
        container.appendChild(cur = document.createElement("input"));
        label.htmlFor = cur.id = prefix + "-" + info[i].name;
        cur.className = prefix + "-int " + prefix + "-" + info[i].name;
        cur.type = "number";
        cur.name = info[i].name;
        cur.min = info[i].min;
        cur.max = info[i].max;
        if ("step" in info[i])
          cur.step = info[i].step;
        cur.value = typeof(def) == "undefined" ? cur.min : def;
        t = cb[info[i].type](info[i]);
        cur.addEventListener("input", t);
        cur.addEventListener("focusout", t);
        break;
      case "boolean":
        container.appendChild(cur = document.createElement("input"));
        label.htmlFor = cur.id = prefix + "-" + info[i].name;
        cur.className = prefix + "-boolean " + prefix + "-" + info[i].name;
        cur.type = "checkbox";
        cur.name = info[i].name;
        cur.defaultChecked = typeof(def) == "undefined" ? false : def;
        if (!("nostyle" in options && options.nostyle))
          cur.style.justifySelf = "start";
        break;
      case "regionlist":
      case "list":
        container.appendChild(cur = document.createElement("select"));
        label.htmlFor = cur.id = prefix + "-" + info[i].name;
        cur.className = prefix + "-list " + prefix + "-" + info[i].name;
        cur.name = info[i].name;
        def = typeof(def) == "undefined" ? 0 : def;
        l = region in info[i].values ? info[i].values[region] : info[i].values;
        for (j = 0; j < l.length; j++) {
          cur.appendChild(t = document.createElement("option"));
          t.selected = j == def;
          t.append(l[j]);
        }
        break;
      case "bits":
        def = typeof(def) == "undefined" ? 0 : def;
        container.appendChild(t = document.createElement("div"));
        t.id = prefix + "-" + info[i].name;
        t.className = prefix + "-bits " + prefix + "-" + info[i].name;
        if (!("nostyle" in options && options.nostyle)) {
          t.style.gridColumnEnd = "span " + (options.columns * 2 - 1);
          t.style.display = "grid";
          t.style.gridTemplateColumns =
              "repeat(" + Math.round(options.columns * 5 / 3) + ", 1fr auto)";
        }
        for (j = 0, l = info[i].values; j < l.length; j++) {
          t.appendChild(label = document.createElement("label"));
          label.append(l[j] + ":");
          if (!("nostyle" in options && options.nostyle))
            label.style.textAlign = "right";
          t.appendChild(cur = document.createElement("input"));
          cur.type = "checkbox";
          cur.name = info[i].name + "-" + j;
          label.htmlFor = cur.id = prefix + "-" + cur.name;
          cur.defaultChecked = !!(def & 1 << j);
          if (!("nostyle" in options && options.nostyle))
            cur.style.justifySelf = "start";
        }
        if (!("nostyle" in options && options.nostyle)) {
          // Pad the first row to help with alignment.
          for (; j < Math.round(options.columns * 5 / 3); j++) {
            t.appendChild(cur = document.createElement("br"));
            t.appendChild(cur = document.createElement("input"));
            cur.type = "checkbox";
            cur.disabled = true;
            cur.style.visibility = "hidden";
          }
        }
        break;
      case "hidden":
        container.appendChild(cur = document.createElement("input"));
        cur.className = prefix + "-hidden " + prefix + "-" + info[i].name;
        cur.type = "hidden";
        cur.name = info[i].name;
        cur.value = typeof(def) == "undefined" ? 0 : def;
        break;
      }
    }
    root.appendChild(cur = document.createElement("p"));
    cur.className = prefix + "-errors";
    if (!("nostyle" in options && options.nostyle))
      cur.style.textAlign = "center";
    uifindname(root, "region-name").addEventListener("change", function (e) {
      window.location.hash = prefix + "-" +
          uiupdateregion(root, game, locale, prefix) + game;
    });
    uiupdateregion(root, game, locale, prefix);
    return root;
  }

  // 'uitabs' creates lazily loaded "tabs" for each game , defaulting to 'game',
  // with 'uicreate' with the same arguments given and returns a "div" of class
  // "'prefix'-games" containing both the "tabs" and the "div" from 'uicreate'.
  // If the url has a fragment of "'prefix'-'region''game'" then it overrides
  // the default values with those and scrolls to the resulting "div" on "load".
  function uitabs(region, game = 1, prefix = "bokpass", locale = "en",
      options = {}) {
    var root, cur, t, i;
    var hash = document.location.hash.replace(/^#/, "");
    if (hash.indexOf(prefix) == 0) {
      hash = hash.slice(prefix.length).replace(/^[-_]+/, "");
      try {
        region = ["jp", "na", "eu"][parseregion(hash.replace(/\d+$/, "")) - 1];
      } catch (e) {}
      hash = hash.replace(/.*(\d+)$/, "$1");
      if (hash != "" && !isNaN(hash = parseInt(hash)) && hash >= 1 && hash <= 4)
        game = hash;
      hash = true;
    } else
      hash = false;
    root = document.createElement("div");
    root.className = prefix + "-games";
    root.appendChild(cur = document.createElement("select"));
    cur.name = "game-tabs";
    cur.className = prefix + "-tabs";
    cur.style.display = "block";
    cur.style.width = "100%";
    for (i = 0; i < strings[locale].labels["game-tabs"].length; i++) {
      cur.appendChild(t = document.createElement("option"));
      t.value = strings[locale].labels["game-tabs"][i].game;
      t.append(strings[locale].labels["game-tabs"][i].name);
      t.selected = t.value == game;
    }
    cur.addEventListener("change", (function () {
      var tabs = {};
      var cur = undefined;
      return function (e) {
        uiallowkeyboards(prefix);
        if (!(e.target.value in tabs))
          tabs[e.target.value] =
              uicreate(region, parseInt(e.target.value), prefix, locale,
                  options);
        if (typeof(cur) == "undefined")
          root.appendChild(tabs[e.target.value]);
        else
          root.replaceChild(tabs[e.target.value], cur);
        cur = tabs[e.target.value];
      };
    })());
    cur.dispatchEvent(new Event("change", {bubble: true, cancelable: true}));
    if (hash)
      window.addEventListener("load", function () {root.scrollIntoView()});
    cur.addEventListener("change", function (e) {
        window.location.hash = prefix + "-" + uiregion(root, locale).code +
          e.target.value;
    });
    return root;
  }

  // 'keyboardpress' modifies the currently selected text input as if 'c' were
  // pressed.  If 'c' is in 'keyboards.alias' it is changed before processing.
  // Supported special values are "ArrowUp", "ArrowDown", "ArrowLeft",
  // "ArrowRight", "Backspace", and "Delete".
  function keyboardpress(c) {
    var t = document.activeElement, s, e;
    c = c in keyboards.alias ? keyboards.alias[c] : c;
    if (!("selectionStart" in t))
      return;
    s = t.selectionStart;
    e = t.selectionEnd;
    switch (c) {
    case "ArrowUp":
      s = 0;
      break;
    case "ArrowDown":
      s = t.value.length;
      break;
    case "ArrowLeft":
      s--;
      break;
    case "ArrowRight":
      s = e + 1;
      break;
    case "Backspace":
      if (s != e)
        t.value = t.value.slice(0, s) + t.value.slice(e);
      else if (t.selectionStart > 0)
        t.value = t.value.slice(0, --s) + t.value.slice(e);
      break;
    case "Delete":
      if (t.selectionStart != t.selectionEnd)
        t.value = t.value.slice(0, s) + t.value.slice(e);
      else if (t.selectionStart > 0)
        t.value = t.value.slice(0, s++) + t.value.slice(e + 1);
      break;
    default:
      t.value = t.value.slice(0, s++) + c + t.value.slice(e);
      break;
    }
    t.setSelectionRange(s, s);
    t.dispatchEvent(new Event("input", {bubble: true, cancelable: true}));
  }

  // 'keyboardcreate' initializes a grid of buttons from 'keys' that call
  // 'keyboardpress' with their value.
  // 'keys' is an array of strings, or the name of a keyboard in 'keyboards',
  // If a key is a string in 'keyboards.expand' it is expanded to fill the grid
  // columns.
  function keyboardcreate(keys, prefix = "bokpass") {
    var root, cur;
    var max, diff, exp = [];
    var col, i, j, k, span;
    var cb = function (e) {
      keyboardpress(e.target.value)
      e.preventDefault();
    };
    if (typeof(keys) == "string" && keys in keyboards)
      keys = keyboards[keys];
    root = document.createElement("div");
    root.className = prefix + "-keyboard";
    root.style.display = "grid";
    root.style.marginLeft = root.style.marginRight = "auto";
    root.style.width = "fit-content";
    for (i = 0, max = 1; i < keys.length; i++) {
      max = keys[i].length > max ? keys[i].length : max;
      for (j = 0, exp[i] = 0; j < keys[i].length; j++)
        if (keys[i][j] in keyboards.expand && keyboards.expand[keys[i][j]])
          exp[i]++;
    }
    root.style.gridTemplateColumns = "repeat(" + max + ", 1.75em)";
    root.style.gridTemplateRows = "repeat(" + keys.length + ", 1.75em)";
    for (i = 0; i < keys.length; i++) {
      diff = max - keys[i].length;
      for (j = 0, col = 1, k = 0; j < keys[i].length; j++, col += span) {
        if (keys[i][j] == "") {
          span = 1;
          continue;
        }
        root.appendChild(cur = document.createElement("input"));
        cur.type = "button";
        cur.value = keys[i][j];
        cur.style.font = "large mono";
        cur.style.gridRow = i + 1;
        span = 1;
        if (keys[i][j] in keyboards.expand && keyboards.expand[keys[i][j]])
          span += (diff / exp[i] | 0) + (++k == exp[i] ? diff % exp[i] : 0);
        if (span == 1)
          cur.style.gridColumn = col;
        else
          cur.style.gridColumn = col + " / span " + span;
        cur.addEventListener("mousedown", cb);
      }
    }
    return root;
  }

  // 'keyboardtabs' creates lazily loaded "tabs" for each keyboard with 'prefix'
  // and 'locale' and returns a "div" of class "'prefix'-keyboards" containing
  // both the "tabs" and the "div" from 'keyboardcreate'.
  function keyboardtabs(prefix = "bokpass", locale = "en") {
    var root, cur, t;
    var i;
    root = document.createElement("div");
    root.className = prefix + "-keyboards";
    root.appendChild(cur = document.createElement("select"));
    cur.name = "keyboard-tabs";
    cur.className = prefix + "-tabs";
    cur.style.display = "block";
    cur.style.width = "100%";
    for (i = 0; i < strings[locale].labels["keyboard-tabs"].length; i++) {
      cur.appendChild(t = document.createElement("option"));
      t.value = strings[locale].labels["keyboard-tabs"][i].key;
      t.append(strings[locale].labels["keyboard-tabs"][i].name);
      t.selected = i == 0;
    }
    cur.addEventListener("change", (function () {
      var tabs = {};
      var cur = undefined;
      return function (e) {
        if (!(e.target.value in tabs))
          tabs[e.target.value] = keyboardcreate(e.target.value, prefix);
        if (typeof(cur) == "undefined")
          root.appendChild(tabs[e.target.value]);
        else
          root.replaceChild(tabs[e.target.value], cur);
        cur = tabs[e.target.value];
      };
    })());
    cur.dispatchEvent(new Event("change"));
    return root;
  }

  // 'uicreateall' returns a "div" with class 'prefix' containing the values
  // from calling 'uitabs' and 'keyboardtabs'.
  function uicreateall(region, game = 1, prefix = "bokpass", locale = "en",
      options = {}) {
    var root = document.createElement("div");
    root.class = prefix;
    root.id = prefix;
    root.appendChild(uitabs(region, game, prefix, locale, options));
    root.appendChild(keyboardtabs(prefix, locale));
    return root;
  }

  return {
    dec: { b64: decb64, str: decstr, pass: decpass },
    enc: { b64: encb64, str: encstr, pass: encpass },
    get: {
      int: getint, str: getstr, keys: getkeys, key: getkey, dict: getdict
    },
    put: { int: putint, str: putstr, dict: putdict },
    checksum: checksum,
    xor: xor,
    tabs: { ui: uitabs, kbd: keyboardtabs },
    create: { ui: uicreate, kbd: keyboardcreate, all: uicreateall }
  };
})();
